/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package co.edu.unal.ahnmodeler.model;

import co.edu.unal.ahnmodeler.main.App;
import co.edu.unal.ahnmodeler.util.RandomObjects;
import co.edu.unal.ahnmodeler.view.PModel;
import java.awt.Point;
import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Clase que representa la red Ad-Hoc en un instante de tiempo determinado.
 * @author Phelipe
 */
public class Network {
    private static Network INSTANCE = null;
    
    // creador sincronizado para protegerse de posibles problemas  multi-hilo
    // otra prueba para evitar instanciación múltiple 
    private synchronized static void createInstance() {
        if (INSTANCE == null) { 
            INSTANCE = new Network();
        }
    }
 
    /**
     * Permite obtener la instancia de la red Ad-Hoc en un momento determinado.
     * @return Instancia de la red Ad-Hoc en un instante determinado.
     */
    public static Network getInstance() {
        createInstance();
        return INSTANCE;
    }
    
    private Network(){ 
        nodes = new CopyOnWriteArrayList<>();
        connections =  new CopyOnWriteArrayList<>();
        sentPackages=0;
        receivedPackages=0;
        jumps=0;
        sendingTime=0;
    }
    
    /**
     * Permite agregar y eliminar nodos aleatoriamente a la red Ad-Hoc en un doubleervalo de tiempo determinado.
     */
    public void initRandomManagerNodes(){
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                while (run) {
                    if (RandomObjects.randomBoolean()) {
                        addNode();
                    } else if (nodes.size()>0){
                        Node nodeToDelete = RandomObjects.randomNode(nodes);
                        deleteNode(nodeToDelete);
                    }
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ex) {
                        Logger.getLogger(PModel.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }
        });
        thread.start();
    }
    
    /**
     * Permite iniciar el envío de paquetes de un nodo origen a un nodo destino.
     */
    public void initSendingData(){
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                while (run) {
                    updateStats();
                    try {
                        if (nodes.size() > 1) {
                            Node nodeSource = RandomObjects.randomNode(nodes);
                            Node nodeDestination = RandomObjects.randomNode(nodes);
                            while (nodeSource == nodeDestination) {
                                nodeDestination = RandomObjects.randomNode(nodes);
                            }
                            sentPackages++;
                            nodeSource.setSending(true);
                            nodeSource.setRoute(new ArrayList());
                            nodeSource.sendData(nodeSource, nodeDestination);
                            int time = 0;
                            while(nodeSource.isSending()&& (time < WAITING_TIME)) {
                                Thread.sleep(1);
                                time++;
                            }
                            if (!nodeSource.isSending()) {
                                receivedPackages++;
                                jumps += nodeSource.getRoute().size();
                                for (Node node : nodeSource.getRoute()) {
                                    int t = RandomObjects.randomWaitingTime();
                                    Thread.sleep(t);
                                    time+=t;
                                    node.decreasePower();
                                }
                                sendingTime += time;
                            } else {
                                nodeSource.setSending(false);
                            }
                        }
                    Thread.sleep(WAITING_TIME*2);
                    } catch (InterruptedException ex) {
                        Logger.getLogger(PModel.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }
        });
        thread.start();
    }
    
    /**
     * Permite agregar un nuevo nodo a la red Ad-Hoc. El nodo inicia en un punto aleatorio del modelo, e inicia un hilo que le permite moverse a través de la red Ad-Hoc e doubleeractuar con otros nodos.
     */
    public void addNode(){
        Node node = new Node();
        nodes.add(node);
        if(run){
            node.setRunning(true);
            Thread thread=new Thread(node);
            thread.start();
        }
    }
    
    /**
     * Permite iniciar la simulación. Inicia el movimiento de todos los nodos en la red Ad-Hoc.
     */
    public void startSimulation(){
        run = true;
        for (Node node : nodes) {
            node.setRunning(true);
            Thread thread=new Thread(node);
            thread.start();
        }
        initRandomManagerNodes();
        initSendingData();
    }
    
    /**
     * Permite detener la simulación. Detiene el movimiento de todos los nodos en la red Ad-Hoc.
     */
    public void stopSimulation(){
        run = false;
        for (Node node : nodes) {
            node.setRunning(false);
        }
        String status = "Simulación terminada";
        App.setStatus(status);
        App.getmFrame().getsBar().setRunning(false);
    }
    
    /**
     * Eliminar la red Ad-Hoc. Detiene la simulación. Remueve todos los nodos que conforman la red Ad-Hoc, así como las conexiones entre ellos. Reinicializa los valores de la simulación.
     */
    public void deleteNetwork(){
        this.stopSimulation();
        connections = new CopyOnWriteArrayList<>();
        nodes = new CopyOnWriteArrayList<>();
        jumps=0;
        sentPackages=0;
        receivedPackages=0;
    }
    
    /**
     * Elimina el nodo que se recibe como parámetro.
     * @param node Nodo a eliminar de la red Ad-Hoc.
     */
    public void deleteNode(Node node){
        node.setRunning(false);
        nodes.remove(node);
    }
    
    /**
     * Elimina aleatoriamente un nodo de la red Ad-Hoc.
     */
    public void deleteNode(){
        this.deleteConnections();
        CopyOnWriteArrayList<Node> remainNodes=new CopyOnWriteArrayList();
        for (Node node : nodes) {
            if (!node.isSelected()) {
                remainNodes.add(node);
            }else{
                node.setRunning(false);
            }
        }
        nodes=remainNodes;
    }
    
    /**
     * Deselecciona todos los nodos que estan seleccionados en la red Ad-Hoc en un momento determinado.
     */
    public void deselectNode(){
        for (Node node : nodes) {
            node.setSelected(false);
        }
    }
    
    /**
     * Determina las conexiones existentes entre los nodos que componen la red Ad-Hoc.
     * @param nodes Nodos que conforman la red Ad-Hoc.
     */
    public synchronized void addConnections(CopyOnWriteArrayList<Node>nodes){
        connections=new CopyOnWriteArrayList();
        for (Node a : nodes) {
            for (Node b : nodes) {
                if (a!=b) {
                    if (Point.distance(a.getStartPoint().x, a.getStartPoint().y, b.getStartPoint().x, b.getStartPoint().y) < Math.min(a.getRange(),b.getRange())){
                        connections.add(new Connection(a, b));
                    }
                }
            }
        }
    }

    /**
     * Permite obtener las conexiones existentes en la red Ad-Hoc en un instante de tiempo determinado.
     * @return Conexiones existentes entre los nodos que conforman la red Ad-Hoc en un instante determinado de tiempo.
     */
    public CopyOnWriteArrayList<Connection> getConnections() {
        return connections;
    }
    
    /**
     * Eliminar las conexiones entre los nodos que conforman la red Ad-Hoc.
     */
    public void deleteConnections(){
        CopyOnWriteArrayList<Connection> remainConnections=new CopyOnWriteArrayList();
        for (Connection connection : connections) {
            if (!connection.getOrigin().isSelected() && !connection.getTarget().isSelected()) {
                remainConnections.add(connection);
            }
        }
        connections=remainConnections;
    }
    
    /**
     * Permite determinar las conexiones existentes entre los nodos que componen la red Ad-Hoc.
     */
    public void addConnetions(){
        this.addConnections(nodes);
    }

    /**
     * Permite obtener la lista de nodos que conforman la red Ad-Hoc en un instante de tiempo determinado.
     * @return Lista de nodos que conforman la red Ad-Hoc en un instante de tiempo determinado.
     */
    public CopyOnWriteArrayList<Node> getNodes() {
        return nodes;
    }
    
    /**
     * Permite obtener el número total de nodos que conforman la red Ad-Hoc en un instante de tiempo determinado.
     * @return Número total de nodos que conforman la red Ad-Hoc en un instante de tiempo determinado.
     */
    public int getNumNodes(){
        return nodes.size();
    }
    
    /**
     * Permite obtener el número total de conexiones existentes entre los nodos que conforman la red Ad-Hoc.
     * @return Número total de conexiones existentes en la red Ad-Hoc.
     */
    public int getNumCon(){
        return connections.size()/2;
    }

    /**
     * Permite determinar si existe una simulación en ejecución.
     * @return Si existe una simulación en ejecución retorna <i>True</i>, de lo contrario retorna <i>False</i>.
     */
    public boolean isRun() {
        return run;
    }

    /**
     * Permite asignar si se encuentra en ejecución una simulación.
     * @param run Estado de la simulación de la red Ad-Hoc.
     */
    public void setRun(boolean run) {
        this.run = run;
    }

    /**
     * Permite obtener el número total de paquetes enviados.
     * @return Número total de paquetes enviados.
     */
    public double getSentPackages() {
        return sentPackages;
    }

    /**
     * Permite obtener el número total de paquetes recibidos con éxito.
     * @return Número total de paquetes recibidos con éxito
     */
    public double getReceivedPackages() {
        return receivedPackages;
    }
    
    /**
     * Permite obtener el número promedio de saltos.
     * @return Número promedio de saltos.
     */
    public double averageJumps(){
        return receivedPackages>0?jumps/receivedPackages:0;
    }
    
    /**
     * Permite actualizar las medidas de desempeño de la red Ad-Hoc en un momento determinado de tiempo.
     */
    public void updateStats(){
        App.getmFrame().updatePackagesMeasurement(receivedPackages,sentPackages);
        App.getmFrame().updateAvgJumps(averageJumps());
        App.getmFrame().updateAvgTime(receivedPackages>0?sendingTime/receivedPackages:0);
    }
    
    private CopyOnWriteArrayList<Node> nodes;
    private CopyOnWriteArrayList<Connection>connections;
    private boolean run;
    private double sentPackages;
    private double receivedPackages;
    private double jumps;
    private double sendingTime;
    private static final int WAITING_TIME=500;
}